﻿/*
 * @author: wizardc
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System.Text;
using Dou.Utils;
using UnityEngine;

namespace Dou.Download
{
    /// <summary>
    /// 下载管理器，支持多线程和断点续传
    /// 下载中的文件会存放到特定的目录下等待下载完毕后再移入最终的目录下
    /// 下载信息也会存储在该目录下，清理无效数据时是判断不在下载信息中的文件会被删除
    /// 可继续优化实现的功能：
    /// 1. 可以将一个大文件切成多个小文件来多线程下载，最后合并成一个，可以加快下载速度；
    /// 2. 下载优先级；
    /// </summary>
    public class DownloadManager
    {
        private static DownloadManager _instance;
        public static DownloadManager instance => _instance ??= new DownloadManager();

        private const string InfoListFile = "info.asset";

        private static string _infoListPath;
        private static string infoListPath => _infoListPath ??= GetTempPath(InfoListFile);

        private static string _tempPath;

        private static string tempPath
        {
            get
            {
                if (_tempPath == null)
                {
#if UNITY_EDITOR
                    _tempPath = "./DownloadTemp";
#else
                    _tempPath = Path.Combine(Application.persistentDataPath, "DownloadTemp");
#endif
                }
                return _tempPath;
            }
        }

        /// <summary>
        /// 获取临时文件的路径
        /// </summary>
        internal static string GetTempPath(string fileName)
        {
            return Path.Combine(tempPath, fileName);
        }

        /// <summary>
        /// 下载完成委托
        /// </summary>
        public delegate void DownloadCompleteCallback(string url, string filePath);

        /// <summary>
        /// 下载进度委托
        /// </summary>
        public delegate void DownloadProgressCallback(string url, string filePath, long currentBytes, long totalBytes);

        /// <summary>
        /// 下载失败委托
        /// </summary>
        public delegate void DownloadErrorCallback(string url, string filePath, DownloadErrorCode errorCode, string errorMsg);

        private int _retryCount = 3;
        private int _maxDownloadThread = 5;

        // 等待进入下载的队列
        private Queue<DownloadInfo> _waitingList = new Queue<DownloadInfo>();

        // 下载中的项目
        private Dictionary<string, DownloadInfo> _downloadingMap = new Dictionary<string, DownloadInfo>();
        private Dictionary<string, WebDownload> _webDownloadMap = new Dictionary<string, WebDownload>();
        private int _downloadingNum = 0;

        // 下载项信息
        private DownloadInfoList _downloadInfoList;

        private DownloadManager()
        {
            InitDownloadingInfo();
        }
        
        /// <summary>
        /// 下载错误时的重试次数
        /// </summary>
        public int retryCount
        {
            set => _retryCount = Mathf.Clamp(value, 0, 99);
            get => _retryCount;
        }

        /// <summary>
        /// 最大允许下载线程数
        /// </summary>
        public int maxDownloadThread
        {
            set => _maxDownloadThread = Mathf.Clamp(value, 1, 99);
            get => _maxDownloadThread;
        }

        /// <summary>
        /// 当前正在下载的文件数量
        /// </summary>
        public int downloadingNum => _downloadingNum;

        /// <summary>
        /// 开始下载文件
        /// </summary>
        /// <param name="url">网络路径</param>
        /// <param name="filePath">文件下载后的路径</param>
        /// <param name="fileSize">文件大小，小于等于 0 时会自动获取文件尺寸</param>
        /// <returns>协程等待对象</returns>
        /// <example>
        /// var request = DownloadManager.instance.Download("https://cdn.cn/a.rar", "C://a.rar", 0);
        /// // 第一种用法无进度信息
        /// yield return request;
        /// if (request.state == DownloadState.Done)
        /// {
        ///     Debug.Log("下载成功");
        /// }
        /// else
        /// {
        ///     Debug.Log("下载失败：" + request.errorCode + ": " + request.errorMsg);
        /// }
        /// // 第二种用法带进度信息
        /// while (request.isDone)
        /// {
        ///     if (request.state == DownloadState.Idle)
        ///     {
        ///         Debug.Log("下载未开始");
        ///         yield return null;
        ///     }
        ///     else if (request.state == DownloadState.Downloading)
        ///     {
        ///         Debug.Log("下载：" + request.currentBytes + "/" + request.totalBytes);
        ///         yield return null;
        ///     }
        ///     else
        ///     {
        ///         break;
        ///     }
        /// }
        /// if (request.state == DownloadState.Done)
        /// {
        ///     Debug.Log("下载成功");
        /// }
        /// else
        /// {
        ///     Debug.Log("下载失败：" + request.errorCode + ": " + request.errorMsg);
        /// }
        /// </example>
        public DownloadRequest Download(string url, string filePath, long fileSize)
        {
            DownloadRequestInfo info = new DownloadRequestInfo();
            info.state = DownloadState.Idle;
            Download(url, filePath, fileSize, (urlParam, filePathParam) =>
            {
                info.state = DownloadState.Done;
            }, (urlParam, filePathParam, currentBytes, totalBytes) =>
            {
                info.state = DownloadState.Downloading;
                info.currentBytes = currentBytes;
                info.totalBytes = totalBytes;
            }, (url, filePath, errorCode, errorMsg) =>
            {
                info.state = DownloadState.Error;
                info.errorCode = errorCode;
                info.errorMsg = errorMsg;
            });
            return new DownloadRequest(info);
        }

        /// <summary>
        /// 开始下载文件
        /// </summary>
        /// <param name="url">网络路径</param>
        /// <param name="filePath">文件下载后的路径</param>
        /// <param name="fileSize">文件大小，小于等于 0 时会自动获取文件尺寸</param>
        /// <param name="onComplete">下载完成回调</param>
        /// <param name="onProgress">下载进度回调</param>
        /// <param name="onError">下载失败回调</param>
        public void Download(string url, string filePath, long fileSize, DownloadCompleteCallback onComplete, DownloadProgressCallback onProgress = null, DownloadErrorCallback onError = null)
        {
            // 获取唯一标识
            string sign = GetSign(url, filePath);

            // 在等待列表时
            DownloadInfo waitingInfo = GetInWaiting(sign);
            if (waitingInfo != null)
            {
                waitingInfo.AddOnComplete(onComplete);
                waitingInfo.AddOnProgress(onProgress);
                waitingInfo.AddOnError(onError);
                return;
            }

            // 在下载列表时
            if (_downloadingMap.ContainsKey(sign))
            {
                DownloadInfo downloadInfo = _downloadingMap[sign];
                downloadInfo.AddOnComplete(onComplete);
                downloadInfo.AddOnProgress(onProgress);
                downloadInfo.AddOnError(onError);
                return;
            }

            // 需要新建下载
            DownloadInfo info = new DownloadInfo();
            info.sign = sign;
            info.url = url;
            info.filePath = filePath;
            info.currentBytes = 0;
            info.totalBytes = fileSize;
            info.retryCount = _retryCount;
            info.AddOnComplete(onComplete);
            info.AddOnProgress(onProgress);
            info.AddOnError(onError);

            _waitingList.Enqueue(info);
            StartDownload();
        }

        private string GetSign(string url, string filePath)
        {
            MD5 md5 = MD5.Create();
            byte[] bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(url + "|" + filePath));
            return BitConverter.ToString(bytes);
        }

        private DownloadInfo GetInWaiting(string sign)
        {
            foreach (var info in _waitingList)
            {
                if (info.sign == sign)
                {
                    return info;
                }
            }
            return null;
        }

        private void StartDownload()
        {
            if (_waitingList.Count == 0)
            {
                return;
            }
            if (_downloadingNum >= _maxDownloadThread)
            {
                return;
            }

            DownloadInfo info = AddDownloadingInfo(_waitingList.Dequeue());
            WebDownload download = new WebDownload();
            _webDownloadMap.Add(info.sign, download);
            download.Start(info, OnDownloadComplete, OnDownloadProgress, OnDownloadError);
        }

        private void OnDownloadComplete(string sign)
        {
            DownloadInfo info = RemoveDownloadingInfo(sign, true);
            RefreshDownloadingInfo();
            _webDownloadMap.Remove(info.sign);
            info.InvokeOnComplete();
            StartDownload();
        }

        private void OnDownloadProgress(string sign, long currentBytes, long totalBytes)
        {
            RefreshDownloadingInfo();
            DownloadInfo info = _downloadingMap[sign];
            info.InvokeOnProgress(currentBytes, totalBytes);
        }

        private void OnDownloadError(string sign, DownloadErrorCode errorCode, string errorMsg)
        {
            DownloadInfo info = RemoveDownloadingInfo(sign, false);
            if (info.retryCount <= 0)
            {
                RefreshDownloadingInfo();
                _webDownloadMap.Remove(info.sign);
                info.InvokeOnError(errorCode, errorMsg);
            }
            else
            {
                info.retryCount--;
                Log.Warning($"\"{info.url}\"下载失败（{errorCode}: {errorMsg}），第{_retryCount - info.retryCount}次重试...");
                _webDownloadMap.Remove(info.sign);
                WebDownload download = new WebDownload();
                _webDownloadMap.Add(info.sign, download);
                download.Start(info, OnDownloadComplete, OnDownloadProgress, OnDownloadError);
            }
            StartDownload();
        }

        private void InitDownloadingInfo()
        {
            Directory.CreateDirectory(tempPath);
            if (File.Exists(infoListPath))
            {
                FileStream file = File.Open(infoListPath, FileMode.Open);
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                _downloadInfoList = binaryFormatter.Deserialize(file) as DownloadInfoList;
                file.Close();
            }
            else
            {
                _downloadInfoList = new DownloadInfoList();
            }
        }

        private DownloadInfo AddDownloadingInfo(DownloadInfo info)
        {
            foreach (DownloadInfo item in _downloadInfoList.list)
            {
                if (item.sign == info.sign)
                {
                    item.CopyFrom(info);
                    _downloadingMap.Add(item.sign, item);
                    _downloadingNum++;
                    return item;
                }
            }

            _downloadingMap.Add(info.sign, info);
            _downloadingNum++;
            _downloadInfoList.list.Add(info);
            return info;
        }

        private DownloadInfo RemoveDownloadingInfo(string sign, bool removeInfoList)
        {
            DownloadInfo info = _downloadingMap[sign];
            _downloadingMap.Remove(sign);
            _downloadingNum--;
            if (removeInfoList)
            {
                RemoveInfoList(info);
            }
            return info;
        }

        private void RemoveInfoList(DownloadInfo info)
        {
            foreach (DownloadInfo downloadInfo in _downloadInfoList.list)
            {
                if (downloadInfo.sign == info.sign)
                {
                    _downloadInfoList.list.Remove(downloadInfo);
                    return;
                }
            }
        }

        private void RefreshDownloadingInfo()
        {
            FileStream file = File.Open(infoListPath, FileMode.OpenOrCreate, FileAccess.Write);
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            binaryFormatter.Serialize(file, _downloadInfoList);
            file.Close();
        }

        /// <summary>
        /// 取消下载
        /// </summary>
        public void Cancel(string url, string filePath)
        {
            string sign = GetSign(url, filePath);
            if (_webDownloadMap.TryGetValue(sign, out WebDownload download))
            {
                download.Cancel();
            }
        }

        /// <summary>
        /// 清除下载临时文件夹
        /// 注意：请在没有任何下载任务时执行
        /// </summary>
        public void ClearTempCache()
        {
            DirectoryInfo dir = new DirectoryInfo(_tempPath);
            if (!dir.Exists)
            {
                return;
            }
            FileInfo[] files = dir.GetFiles();
            foreach (FileInfo file in files)
            {
                file.Delete();
            }
        }
    }
}
